package com.crunch.core;

import com.crunch.graphics.gpu.GpuBuffer;
import com.crunch.graphics.gpu.GpuDrawCall;
import com.crunch.graphics.gpu.GpuState;
import com.crunch.graphics.gpu.GpuViewportState;

import javax.media.opengl.*;
import java.nio.FloatBuffer;
import java.util.*;

public class Renderer {
	private static final boolean DEBUG = false;

	private GpuState gpuState = new GpuState();

	private final int DEFAULT_RENDERABLE_CAPACITY = 100;
	Set<Renderable> renderables = new HashSet<Renderable>();
	Set<Resizable> resizables = new HashSet<Resizable>();
	ArrayList<Renderable> sortedRenderables = new ArrayList<Renderable>(DEFAULT_RENDERABLE_CAPACITY);

	Comparator<Renderable> renderableOrderer = new Comparator<Renderable>() {
		@Override
		public int compare(Renderable o1, Renderable o2) {
			if (o1.getDepth() < o2.getDepth()) {
				return -1;
			} else if (o1.getDepth() > o2.getDepth()) {
				return 1;
			} else {
				return 0;
			}
		}
	};

	// used to execute a specific action at the beginning of the frame - e.g. releasing a texture after onDestroy()
	public static interface Command {
		public void execute(GpuState gpuState);
	}

	private ArrayList<Command> commands = new ArrayList<Command>();

	private void performCommands(GL3 gl) {
		for (Command c : commands) {
			c.execute(gpuState);
		}
		commands.clear();
	}

	public void queueCommand(Command command) {
		commands.add(command);
	}

	public void register(Renderable renderable) {
		renderables.add(renderable);
	}

	public void unregister(Renderable renderable) {
		renderables.remove(renderable);
	}

	public void register(Resizable resizable) {
		resizables.add(resizable);
	}

	public void unregister(Resizable resizable) {
		resizables.remove(resizable);
	}

	public class EventListener implements GLEventListener {
		@Override
		public void init(GLAutoDrawable drawable) {
			try {
				final GL3 gl = drawable.getGL().getGL3();
				if (DEBUG) {
					drawable.setGL(new DebugGL3(gl));
				}

				gpuState.setGl(gl);

				gpuState.initialize();
				utility.initialize();

				gpuState.setGl(null);

				System.out.println("Renderer initialized");
			} catch (GLException e) {
				e.printStackTrace();
			}
		}

		@Override
		public void dispose(GLAutoDrawable drawable) {
			try {
				final GL3 gl = drawable.getGL().getGL3();
				if (DEBUG) {
					drawable.setGL(new DebugGL3(gl));
				}

				gpuState.setGl(gl);

				performCommands(gl);
				utility.terminate();
				gpuState.terminate();

				gpuState.setGl(null);

				System.out.println("Renderer terminated");
			} catch (GLException e) {
				e.printStackTrace();
			}
		}

		@Override
		public void display(GLAutoDrawable drawable) {
			try {
				final GL3 gl = drawable.getGL().getGL3();
				if (DEBUG) {
					drawable.setGL(new DebugGL3(gl));
				}

				gpuState.setGl(gl);

				performCommands(gl);
				Iterator<Renderable> it = renderables.iterator();
				while (it.hasNext()) {
					Renderable r = it.next();
					sortedRenderables.add(r);
				}
				Collections.sort(sortedRenderables, renderableOrderer);
				for (Renderable r : sortedRenderables) {
					r.render(gpuState);
				}
				sortedRenderables.clear();

				gpuState.setGl(null);
			} catch (GLException e) {
				e.printStackTrace();
			}
		}

		@Override
		public void reshape(GLAutoDrawable drawable, int x, int y, int width, int height) {
			try {
				final GL3 gl = drawable.getGL().getGL3();
				if (DEBUG) {
					drawable.setGL(new DebugGL3(gl));
				}

				gpuState.setGl(gl);

				// unfortunately, it appears that JOGL automatically calls glViewport(x, y, width, height) before this is called
				// they do this for convenience, but it actually causes problems for us, since we track OpenGL state
				// therefore, we must reflect this change here
				GpuViewportState vpState = gpuState.getCurrentViewportState();
				vpState.setViewport(x, y, width, height);
				vpState.bind();

				// alert resizables of resize
				for (Resizable r : resizables) {
					r.resize(gpuState, x, y, width, height);
				}

				gpuState.setGl(gl);
			} catch (GLException e) {
				e.printStackTrace();
			}
		}
	}

	private EventListener eventListener = new EventListener();
	public EventListener getEventListener() { return eventListener; }

	public class Utility {
		private GpuBuffer spriteVertexBuffer;
		public GpuBuffer getSpriteVertexBuffer() { return spriteVertexBuffer; }

		private void initialize() {
			FloatBuffer vBuf = FloatBuffer.wrap(new float[]{
					0.0f, 0.0f,
					1.0f, 0.0f,
					1.0f, 1.0f,
					0.0f, 1.0f
			});
			GpuBuffer.BufferDescription desc = new GpuBuffer.BufferDescription();
			desc.type = GL.GL_ARRAY_BUFFER;
			desc.size = vBuf.capacity() * (Float.SIZE / Byte.SIZE);
			desc.data = vBuf;
			desc.usage = GL.GL_STATIC_DRAW;
			spriteVertexBuffer = gpuState.createBuffer(desc);
		}

		private void terminate() {
			spriteVertexBuffer.release();
		}

		private float[] clearColor = new float[4];
		public void setClearColor(float[] col) {
			System.arraycopy(col, 0, clearColor, 0, 4);
		}

		public Renderable getClearRenderable() {
			return new Renderable() {
				@Override
				public void render(GpuState gpuState) {
					GpuDrawCall.ClearCall cc = gpuState.createClearCall();
					cc.setClearBuffers(GL.GL_COLOR_BUFFER_BIT);
					cc.setClearColor(clearColor);
					cc.clear();
				}

				@Override
				public int getDepth() {
					return Integer.MIN_VALUE;
				}
			};
		}
	}

	private Utility utility = new Utility();
	public Utility getUtility() { return utility; }
}
